/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.ggdi;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.cfg.ConfigurationProperty;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.cfg.Property;
import com.google.inject.Module;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServlet;

/**
 * Utility methods for discovering Guice modules from GWT modules.
 *
 * @author jgennis
 */
class Utils {

  static final String GUICE_MODULES_PROP = "com.google.code.ggdi.guiceModules";

  static final String TEST_GUICE_MODULES_PROP_SUFFIX = ".testGuiceModules";

  /**
   * Get a GWT module definition for a given module name.
   * 
   * @param logger the logger to use while loading the GWT module
   * @param moduleName the name of the GWT module to fetch
   * @return the module definition
   * @throws ConfigurationException if the module could not be loaded
   */
  public static ModuleDef getModuleDef(TreeLogger logger, String moduleName)
      throws ConfigurationException {
    try {
      ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger, moduleName, false);
      return moduleDef;
    } catch (UnableToCompleteException ex) {
      throw new ConfigurationException("Unable to load GWT module '" + moduleName + "'", ex);
    }
  }

  /**
   * Get the Guice modules needed for a given GWT module.  This does not
   * include the Guice modules that are only needed for testing the GWT module.
   * 
   * @param moduleDef the GWT module definition
   * @return a list containing the Guice modules required to run the GWT module
   * @throws ConfigurationException if the module list could not be created
   */
  public static List<Module> getGuiceModules(ModuleDef moduleDef) throws ConfigurationException {
    Set<String> guiceModuleNames = getGuiceModuleNames(moduleDef, GUICE_MODULES_PROP);
    List<Module> guiceModules = getGuiceModules(guiceModuleNames);
    return guiceModules;
  }

  /**
   * Get the Guice modules needed to run a {@code GWTTestCase} using a given
   * GWT module.
   *
   * @param moduleDef the GWT module definition
   * @return a list of Guice modules required to test the GWT module
   * @throws ConfigurationException if the module list could not be created
   */
  public static List<Module> getTestGuiceModules(ModuleDef moduleDef)
      throws ConfigurationException {
    String moduleName = moduleDef.getCanonicalName();
    if (!moduleName.endsWith(".JUnit")) {
      throw new ConfigurationException("Cannot get test Guice modules for non-JUnit GWT module '" +
          moduleName + "'");
    }
    String baseModuleName = moduleName.substring(0, moduleName.length() - ".JUnit".length());
    String testGuiceModulesProp = baseModuleName + TEST_GUICE_MODULES_PROP_SUFFIX;
    Set<String> testGuiceModuleNames;
    try {
      testGuiceModuleNames = getGuiceModuleNames(moduleDef, testGuiceModulesProp);
    } catch (ConfigurationException ex) {
      return new ArrayList<Module>();
    }
    Set<String> guiceModuleNames = getGuiceModuleNames(moduleDef, GUICE_MODULES_PROP);
    testGuiceModuleNames.removeAll(guiceModuleNames); // exclude non-test modules
    List<Module> testGuiceModules = getGuiceModules(testGuiceModuleNames);
    for (Module testGuiceModule : testGuiceModules) {
      Class<? extends Module> moduleClass = testGuiceModule.getClass();
      GwtModule gwtModuleAnnot = moduleClass.getAnnotation(GwtModule.class);
      String gwtModuleName = gwtModuleAnnot.value();
      if (!gwtModuleName.equals(baseModuleName)) {
        throw new ConfigurationException("The @GwtModule annotation on test Guice module class '" +
            moduleClass.getCanonicalName() + "' does not match the GWT module that references " +
            "it: '" + baseModuleName + "'");
      }
    }
    return testGuiceModules;
  }

  /**
   * Get the a {@code Map} of the servlet bindings.  The {@code Map} maps
   * servlet URI paths to a {@code HttpServlet} class.
   *
   * @param logger the logger to use while loading GWT modules
   * @param guiceModules the Guice modules for which the servlet bindings
   *        should be gathered
   * @return a map from servlet URIs to an {@code HttpServlet} class
   * @throws ConfigurationException if the servlet binding could not be
   *         determined
   */
  public static Map<String, Class<? extends HttpServlet>> getServletBindings(TreeLogger logger,
      List<Module> guiceModules) throws ConfigurationException {
    Map<String, Class<? extends HttpServlet>> bindings =
        new HashMap<String, Class<? extends HttpServlet>>();

    for (Module guiceModule : guiceModules) {
      GwtModule gwtModule = guiceModule.getClass().getAnnotation(GwtModule.class);
      if (gwtModule != null) {
        String gwtModuleName = gwtModule.value();
        ModuleDef moduleDef;
        try {
          moduleDef = ModuleDefLoader.loadFromClassPath(logger, gwtModuleName, false);
        } catch (UnableToCompleteException ex) {
          throw new ConfigurationException("Unable to load GWT module definition '" + gwtModuleName +
              "' for Guice module '" + guiceModule.getClass().getCanonicalName() + "'");
        }
        for (String relativePath : moduleDef.getServletPaths()) {
          String servletPath = "/" + moduleDef.getName() + relativePath;
          String servletName = moduleDef.findServletForPath(relativePath);
          Class<? extends HttpServlet> servletClass;
          if (servletName.equals(RemoteServiceTestServlet.class.getCanonicalName()))
            servletClass = GgdiRemoteServiceServlet.class;
          else if (servletName.equals(RpcTestServlet.class.getCanonicalName()))
            servletClass = GgdiRpcServlet.class;
          else
            continue;
          Class<? extends HttpServlet> existingBinding = bindings.get(servletPath);
          if (existingBinding != null && existingBinding != servletClass) {
            throw new ConfigurationException("Multiple servlet mappings founds for path '" +
                servletPath + "':\n  " + existingBinding.getCanonicalName() + "\n  " +
                servletClass.getCanonicalName());
          }
          bindings.put(servletPath, servletClass);
        }
      }
    }
    return bindings;
  }

  /**
   * Get the set of Guice module names stored in a configuration property of a
   * GWT module.
   * 
   * @param moduleDef the GWT module definition
   * @param guiceModulesPropName the name of the configuration property
   *        containing the Guice module names
   * @return the Guice module names contained in the specified GWT
   *         configuration property
   * @throws ConfigurationException if the Guice module names could not be
   *         determined
   */
  static Set<String> getGuiceModuleNames(ModuleDef moduleDef, String guiceModulesPropName)
      throws ConfigurationException {
    String moduleCanonicalName = moduleDef.getCanonicalName();
    Property guiceModulesProp = moduleDef.getProperties().find(guiceModulesPropName);
    if (guiceModulesProp == null) {
      throw new ConfigurationException("The '" + guiceModulesPropName + "' property is not set " +
          "for module '" + moduleCanonicalName + "'.  Did you forget to inherit the " +
          "GuiceDiscovery GWT module?");
    }
    if (!(guiceModulesProp instanceof ConfigurationProperty)) {
      throw new ConfigurationException("The '" + guiceModulesPropName + "' property is not a " +
          "configuration property in module '" + moduleCanonicalName + "'.  Did you forget to " +
          "inherit the GuiceDiscovery GWT module?");
    }
    List<String> guiceModuleNamesList = ((ConfigurationProperty) guiceModulesProp).getValues();
    Set<String> guiceModuleNames = new HashSet<String>(guiceModuleNamesList);
    return guiceModuleNames;
  }

  /**
   * Get a list of Guice modules corresponding to some Guice module names.
   * 
   * @param guiceModuleNames the Guice module names for which to get modules
   * @return the Guice modules corresponding to the given Guice module names
   * @throws ConfigurationException if the Guice modules could not be created
   */
  static List<Module> getGuiceModules(Set<String> guiceModuleNames) throws ConfigurationException {
    List<Module> guiceModules = new ArrayList<Module>();
    for (String guiceModuleName : guiceModuleNames) {
      Module guiceModule = getGuiceModule(guiceModuleName);
      guiceModules.add(guiceModule);
    }
    return guiceModules;
  }

  /**
   * Get a single Guice module from a module name.
   * 
   * @param guiceModuleName the Guice module name to fetch
   * @return the Guice module corresponding to the given module name
   * @throws ConfigurationException if the Guice module could not be created
   */
  static Module getGuiceModule(String guiceModuleName) throws ConfigurationException {
    Class<?> guiceModuleClass;
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    try {
      guiceModuleClass = classLoader.loadClass(guiceModuleName);
    } catch (ClassNotFoundException ex) {
      throw new ConfigurationException("Could not load Guice module class '" + guiceModuleName +
          "'", ex);
    }
    if (!guiceModuleClass.isAnnotationPresent(GwtModule.class)) {
      throw new ConfigurationException("Guice module class '" + guiceModuleName +
          "' does not have " +
          "a '" + GwtModule.class.getCanonicalName() + "' annotation");
    }
    Constructor<?> constructor;
    try {
      constructor = guiceModuleClass.getConstructor();
    } catch (NoSuchMethodException ex) {
      throw new ConfigurationException("Guice module class '" + guiceModuleName +
          "' does not have " +
          "a public zero-argument constructor", ex);
    } catch (SecurityException ex) {
      throw new RuntimeException(ex);
    }
    Object guiceModuleObj;
    try {
      guiceModuleObj = constructor.newInstance();
    } catch (InstantiationException ex) {
      throw new RuntimeException(ex);
    } catch (IllegalAccessException ex) {
      throw new RuntimeException(ex);
    } catch (InvocationTargetException ex) {
      throw new ConfigurationException("The constructor for Guice module class '" + guiceModuleName +
          "' threw an exception", ex);
    }
    Module guiceModule = (Module) guiceModuleObj;
    if (guiceModule == null) {
      throw new ConfigurationException("Guice module class '" + guiceModuleName + "' does not " +
          "implement the '" + Module.class.getCanonicalName() + "' interface");
    }
    return guiceModule;
  }
}
